// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.AspNetCore.Mvc.IntegrationTests;

public class ValidationIntegrationTests
{
    private class TransferInfo
    {
        [Range(25, 50)]
        public int AccountId { get; set; }

        public double Amount { get; set; }
    }

    private class TestController { }

    public static TheoryData<List<ParameterDescriptor>> MultipleActionParametersAndValidationData
    {
        get
        {
            return new TheoryData<List<ParameterDescriptor>>
                {
                    // Irrespective of the order in which the parameters are defined on the action,
                    // the validation on the TransferInfo's AccountId should occur.
                    // Here 'accountId' parameter is bound by the prefix 'accountId' while the 'transferInfo'
                    // property is bound using the empty prefix and the 'TransferInfo' property names.
                    new List<ParameterDescriptor>()
                    {
                        new ParameterDescriptor()
                        {
                            Name = "accountId",
                            ParameterType = typeof(int)
                        },
                        new ParameterDescriptor()
                        {
                            Name = "transferInfo",
                            ParameterType = typeof(TransferInfo),
                            BindingInfo = new BindingInfo()
                            {
                                BindingSource = BindingSource.Body
                            }
                        }
                    },
                    new List<ParameterDescriptor>()
                    {
                        new ParameterDescriptor()
                        {
                            Name = "transferInfo",
                            ParameterType = typeof(TransferInfo),
                            BindingInfo = new BindingInfo()
                            {
                                BindingSource = BindingSource.Body
                            }
                        },
                        new ParameterDescriptor()
                        {
                            Name = "accountId",
                            ParameterType = typeof(int)
                        }
                    }
                };
        }
    }

    [Theory]
    [MemberData(nameof(MultipleActionParametersAndValidationData))]
    public async Task ValidationIsTriggered_OnFromBodyModels(List<ParameterDescriptor> parameters)
    {
        // Arrange
        var actionDescriptor = new ControllerActionDescriptor()
        {
            BoundProperties = new List<ParameterDescriptor>(),
            Parameters = parameters
        };
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();

        var testContext = ModelBindingTestHelper.GetTestContext(
            request =>
            {
                request.QueryString = new QueryString("?accountId=30");
                request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"accountId\": 15,\"amount\": 250.0}"));
                request.ContentType = "application/json";
            },
            actionDescriptor: actionDescriptor);

        var modelState = testContext.ModelState;

        // Act
        foreach (var parameter in parameters)
        {
            await parameterBinder.BindModelAsync(parameter, testContext);
        }

        // Assert
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(
            modelState,
            e => string.Equals(e.Key, "AccountId", StringComparison.OrdinalIgnoreCase)).Value;
        var error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRangeErrorMessage(25, 50, "AccountId"), error.ErrorMessage);
    }

    [Theory]
    [MemberData(nameof(MultipleActionParametersAndValidationData))]
    public async Task MultipleActionParameter_ValidModelState(List<ParameterDescriptor> parameters)
    {
        // Since validation attribute is only present on the FromBody model's property(TransferInfo's AccountId),
        // validation should not trigger for the parameter which is bound from Uri.

        // Arrange
        var actionDescriptor = new ControllerActionDescriptor()
        {
            BoundProperties = new List<ParameterDescriptor>(),
            Parameters = parameters
        };
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();

        var testContext = ModelBindingTestHelper.GetTestContext(
            request =>
            {
                request.QueryString = new QueryString("?accountId=10");
                request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"accountId\": 40,\"amount\": 250.0}"));
                request.ContentType = "application/json";
            },
            actionDescriptor: actionDescriptor);

        var modelState = testContext.ModelState;

        // Act
        foreach (var parameter in parameters)
        {
            await parameterBinder.BindModelAsync(parameter, testContext);
        }

        // Assert
        Assert.True(modelState.IsValid);
    }

    private class Order1
    {
        [Required]
        public string CustomerName { get; set; }
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnSimpleTypeProperty_WithData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order1)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.CustomerName=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order1>(modelBindingResult.Model);
        Assert.Equal("bill", model.CustomerName);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.CustomerName").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnSimpleTypeProperty_NoData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order1)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order1>(modelBindingResult.Model);
        Assert.Null(model.CustomerName);

        Assert.Single(modelState);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "CustomerName").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        var error = Assert.Single(entry.Errors);
        AssertRequiredError("CustomerName", error);
    }

    private class Order2
    {
        [Required]
        public Person2 Customer { get; set; }
    }

    private class Person2
    {
        public string Name { get; set; }
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnPOCOProperty_WithData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order2)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Customer.Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order2>(modelBindingResult.Model);
        Assert.NotNull(model.Customer);
        Assert.Equal("bill", model.Customer.Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnPOCOProperty_NoData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order2)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order2>(modelBindingResult.Model);
        Assert.Null(model.Customer);

        Assert.Single(modelState);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        var error = Assert.Single(entry.Errors);
        AssertRequiredError("Customer", error);
    }

    private class Order3
    {
        public Person3 Customer { get; set; }
    }

    private class Person3
    {
        public int Age { get; set; }

        [Required]
        public string Name { get; set; }
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnNestedSimpleTypeProperty_WithData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order3)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Customer.Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order3>(modelBindingResult.Model);
        Assert.NotNull(model.Customer);
        Assert.Equal("bill", model.Customer.Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnNestedSimpleTypeProperty_NoDataForRequiredProperty()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order3)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            // Force creation of the Customer model.
            request.QueryString = new QueryString("?parameter.Customer.Age=17");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order3>(modelBindingResult.Model);
        Assert.NotNull(model.Customer);
        Assert.Equal(17, model.Customer.Age);
        Assert.Null(model.Customer.Name);

        Assert.Equal(2, modelState.Count);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        var error = Assert.Single(entry.Errors);
        AssertRequiredError("Name", error);
    }

    private class Order4
    {
        [Required]
        public List<Item4> Items { get; set; }
    }

    private class Item4
    {
        public int ItemId { get; set; }
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnCollectionProperty_WithData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order4)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Items[0].ItemId=17");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order4>(modelBindingResult.Model);
        Assert.NotNull(model.Items);
        Assert.Equal(17, Assert.Single(model.Items).ItemId);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "Items[0].ItemId").Value;
        Assert.Equal("17", entry.AttemptedValue);
        Assert.Equal("17", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnCollectionProperty_NoData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order4)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            // Force creation of the Customer model.
            request.QueryString = new QueryString("?");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order4>(modelBindingResult.Model);
        Assert.Null(model.Items);

        Assert.Single(modelState);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "Items").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        var error = Assert.Single(entry.Errors);
        AssertRequiredError("Items", error);
    }

    private class Order5
    {
        [Required]
        public int? ProductId { get; set; }

        public string Name { get; set; }
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnPOCOPropertyOfBoundElement_WithData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(List<Order5>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter[0].ProductId=17");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<Order5>>(modelBindingResult.Model);
        Assert.Equal(17, Assert.Single(model).ProductId);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter[0].ProductId").Value;
        Assert.Equal("17", entry.AttemptedValue);
        Assert.Equal("17", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnPOCOPropertyOfBoundElement_NoDataForRequiredProperty()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(List<Order5>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            // Force creation of the Customer model.
            request.QueryString = new QueryString("?parameter[0].Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<Order5>>(modelBindingResult.Model);
        var item = Assert.Single(model);
        Assert.Null(item.ProductId);
        Assert.Equal("bill", item.Name);

        Assert.Equal(2, modelState.Count);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter[0].ProductId").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        var error = Assert.Single(entry.Errors);
        AssertRequiredError("ProductId", error);
    }

#nullable enable
    private class ParameterInfos
    {
        public void Method(
            string param1,
            string param2 = "sample_data")
        {
        }

        public static ParameterInfo NonNullableParameterInfo
            = typeof(ParameterInfos)!
                .GetMethod(nameof(ParameterInfos.Method))!
                .GetParameters()[0];

        public static ParameterInfo DefaultValueParameterInfo
            = typeof(ParameterInfos)!
                .GetMethod(nameof(ParameterInfos.Method))!
                .GetParameters()[1];
    }
#nullable restore

    [Fact]
    public async Task Validation_RequiredAttribute_OnActionParameter_WithDefaultValue()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ControllerParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(string),
            ParameterInfo = ParameterInfos.DefaultValueParameterInfo
        };

        var testContext = ModelBindingTestHelper.GetTestContext();

        var modelState = testContext.ModelState;

        // Act
        _ = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelState.IsValid);
        Assert.Equal(0, modelState.ErrorCount);
    }

    [Fact]
    public async Task Validation_RequiredAttribute_OnActionParameter_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ControllerParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(string),
            ParameterInfo = ParameterInfos.NonNullableParameterInfo
        };

        var testContext = ModelBindingTestHelper.GetTestContext();

        var modelState = testContext.ModelState;

        // Act
        _ = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.False(modelState.IsValid);
        Assert.Equal(1, modelState.ErrorCount);

        var entry = Assert.Single(modelState, e => e.Key == "parameter").Value;
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
    }

    private class Order6
    {
        [StringLength(5, ErrorMessage = "Too Long.")]
        public string Name { get; set; }
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfPOCO_Valid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order6)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order6>(modelBindingResult.Model);
        Assert.Equal("bill", model.Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfPOCO_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order6)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Name=billybob");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order6>(modelBindingResult.Model);
        Assert.Equal("billybob", model.Name);

        Assert.Single(modelState);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
        Assert.Equal("billybob", entry.AttemptedValue);
        Assert.Equal("billybob", entry.RawValue);

        var error = Assert.Single(entry.Errors);
        Assert.Equal("Too Long.", error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    private class Order7
    {
        public Person7 Customer { get; set; }
    }

    private class Person7
    {
        [StringLength(5, ErrorMessage = "Too Long.")]
        public string Name { get; set; }
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfNestedPOCO_Valid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order7)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Customer.Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order7>(modelBindingResult.Model);
        Assert.Equal("bill", model.Customer.Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfNestedPOCO_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order7)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Customer.Name=billybob");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order7>(modelBindingResult.Model);
        Assert.Equal("billybob", model.Customer.Name);

        Assert.Single(modelState);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Equal("billybob", entry.AttemptedValue);
        Assert.Equal("billybob", entry.RawValue);

        var error = Assert.Single(entry.Errors);
        Assert.Equal("Too Long.", error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfNestedPOCO_NoData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order7)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order7>(modelBindingResult.Model);
        Assert.Null(model.Customer);

        Assert.Empty(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);
    }

    private class Order8
    {
        [ValidatePerson8]
        public Person8 Customer { get; set; }
    }

    private class Person8
    {
        public string Name { get; set; }
    }

    private class ValidatePerson8Attribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (((Person8)value).Name == "bill")
            {
                return null;
            }
            else
            {
                return new ValidationResult("Invalid Person.");
            }
        }
    }

    [Fact]
    public async Task Validation_CustomAttribute_OnPOCOProperty_Valid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order8)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Customer.Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order8>(modelBindingResult.Model);
        Assert.Equal("bill", model.Customer.Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_CustomAttribute_OnPOCOProperty_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order8)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Customer.Name=billybob");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order8>(modelBindingResult.Model);
        Assert.Equal("billybob", model.Customer.Name);

        Assert.Equal(2, modelState.Count);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
        Assert.Equal("billybob", entry.AttemptedValue);
        Assert.Equal("billybob", entry.RawValue);

        entry = Assert.Single(modelState, e => e.Key == "parameter.Customer").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal("Invalid Person.", error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    private class Order9
    {
        [ValidateProducts9]
        public List<Product9> Products { get; set; }
    }

    private class Product9
    {
        public string Name { get; set; }
    }

    private class ValidateProducts9Attribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (((List<Product9>)value)[0].Name == "bill")
            {
                return null;
            }
            else
            {
                return new ValidationResult("Invalid Product.");
            }
        }
    }

    [Fact]
    public async Task Validation_CustomAttribute_OnCollectionElement_Valid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order9)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Products[0].Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order9>(modelBindingResult.Model);
        Assert.Equal("bill", Assert.Single(model.Products).Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Products[0].Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_CustomAttribute_OnCollectionElement_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order9)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter.Products[0].Name=billybob");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order9>(modelBindingResult.Model);
        Assert.Equal("billybob", Assert.Single(model.Products).Name);

        Assert.Equal(2, modelState.Count);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter.Products[0].Name").Value;
        Assert.Equal("billybob", entry.AttemptedValue);
        Assert.Equal("billybob", entry.RawValue);

        entry = Assert.Single(modelState, e => e.Key == "parameter.Products").Value;
        Assert.Null(entry.RawValue);
        Assert.Null(entry.AttemptedValue);
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        var error = Assert.Single(entry.Errors);
        Assert.Equal("Invalid Product.", error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    private class Order10
    {
        [StringLength(5, ErrorMessage = "Too Long.")]
        public string Name { get; set; }
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfCollectionElement_Valid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(List<Order10>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter[0].Name=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<Order10>>(modelBindingResult.Model);
        Assert.Equal("bill", Assert.Single(model).Name);

        Assert.Single(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Name").Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfCollectionElement_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(List<Order10>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter[0].Name=billybob");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<Order10>>(modelBindingResult.Model);
        Assert.Equal("billybob", Assert.Single(model).Name);

        Assert.Single(modelState);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Name").Value;
        Assert.Equal("billybob", entry.AttemptedValue);
        Assert.Equal("billybob", entry.RawValue);

        var error = Assert.Single(entry.Errors);
        Assert.Equal("Too Long.", error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    [Fact]
    public async Task Validation_StringLengthAttribute_OnPropertyOfCollectionElement_NoData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(List<Order10>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<Order10>>(modelBindingResult.Model);
        Assert.Empty(model);

        Assert.Empty(modelState);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);
    }

    private class User
    {
        public int Id { get; set; }

        public uint Zip { get; set; }

    }

    [Fact]
    public async Task Validation_FormatException_ShowsInvalidValueMessage_OnSimpleTypeProperty()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(User)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Id=bill");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<User>(modelBindingResult.Model);
        Assert.Equal(0, model.Id);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var state = Assert.Single(modelState);
        Assert.Equal("Id", state.Key);
        var entry = state.Value;
        Assert.Equal("bill", entry.AttemptedValue);
        Assert.Equal("bill", entry.RawValue);
        Assert.Single(entry.Errors);

        var error = entry.Errors[0];
        Assert.Equal("The value 'bill' is not valid for Id.", error.ErrorMessage);
    }

    [Fact]
    public async Task Validation_OverflowException_ShowsInvalidValueMessage_OnSimpleTypeProperty()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(User)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Zip=-123");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<User>(modelBindingResult.Model);
        Assert.Equal<uint>(0, model.Zip);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.False(modelState.IsValid);

        var state = Assert.Single(modelState);
        Assert.Equal("Zip", state.Key);
        var entry = state.Value;
        Assert.Equal("-123", entry.AttemptedValue);
        Assert.Equal("-123", entry.RawValue);
        Assert.Single(entry.Errors);

        var error = entry.Errors[0];
        Assert.Equal("The value '-123' is not valid for Zip.", error.ErrorMessage);
    }

    private class NeverValid : IValidatableObject
    {
        public string NeverValidProperty { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var result = new ValidationResult(
                $"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " +
                $"to its {nameof(NeverValid)} type.");
            return new[] { result };
        }
    }

    private class NeverValidAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // By default, ValidationVisitor visits _all_ properties within a non-null complex object.
            // But, like most reasonable ValidationAttributes, NeverValidAttribute ignores null property values.
            if (value == null)
            {
                return ValidationResult.Success;
            }

            return new ValidationResult(
                $"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " +
                $"to its associated {nameof(NeverValidAttribute)}.");
        }
    }

    private class ValidateSomeProperties
    {
        [Display(Name = "Not ever valid")]
        public NeverValid NeverValidBecauseType { get; set; }

        [NeverValid]
        [Display(Name = "Never valid")]
        public string NeverValidBecauseAttribute { get; set; }

        [ValidateNever]
        [NeverValid]
        public string ValidateNever { get; set; }

        [ValidateNever]
        public int ValidateNeverLength => ValidateNever.Length;
    }

    [ValidateNever]
    private class ValidateNoProperties : ValidateSomeProperties
    {
    }

    [Fact]
    public async Task IValidatableObject_IsValidated()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomeProperties),
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request => request.QueryString
                = new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}=1"));

        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomeProperties>(result.Model);
        Assert.Equal("1", model.NeverValidBecauseType.NeverValidProperty);

        Assert.False(modelState.IsValid);
        Assert.Equal(1, modelState.ErrorCount);
        Assert.Collection(
            modelState,
            state =>
            {
                Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseType), state.Key);
                Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState);

                var error = Assert.Single(state.Value.Errors);
                Assert.Equal(
                    "'NeverValidBecauseType' (display: 'Not ever valid') is not valid due to its NeverValid type.",
                    error.ErrorMessage);
                Assert.Null(error.Exception);
            },
            state =>
            {
                Assert.Equal(
                    $"{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}",
                    state.Key);
                Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
            });
    }

    [Fact]
    public async Task CustomValidationAttribute_IsValidated()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomeProperties),
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request => request.QueryString
                = new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseAttribute)}=1"));

        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomeProperties>(result.Model);
        Assert.Equal("1", model.NeverValidBecauseAttribute);

        Assert.False(modelState.IsValid);
        Assert.Equal(1, modelState.ErrorCount);
        var kvp = Assert.Single(modelState);
        Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseAttribute), kvp.Key);
        var state = kvp.Value;
        Assert.NotNull(state);
        Assert.Equal(ModelValidationState.Invalid, state.ValidationState);
        var error = Assert.Single(state.Errors);
        Assert.Equal(
            "'NeverValidBecauseAttribute' (display: 'Never valid') is not valid due to its associated NeverValidAttribute.",
            error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    [Fact]
    public async Task ValidateNeverProperty_IsSkipped()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomeProperties),
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request => request.QueryString
                = new QueryString($"?{nameof(ValidateSomeProperties.ValidateNever)}=1"));

        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomeProperties>(result.Model);
        Assert.Equal("1", model.ValidateNever);

        Assert.True(modelState.IsValid);
        var kvp = Assert.Single(modelState);
        Assert.Equal(nameof(ValidateSomeProperties.ValidateNever), kvp.Key);
        var state = kvp.Value;
        Assert.NotNull(state);
        Assert.Equal(ModelValidationState.Skipped, state.ValidationState);
    }

    [Fact]
    public async Task ValidateNeverProperty_IsSkippedWithoutAccessingModel()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomeProperties),
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomeProperties>(result.Model);

        // Note this Exception is not thrown earlier.
        Assert.Throws<NullReferenceException>(() => model.ValidateNeverLength);

        Assert.True(modelState.IsValid);
        Assert.Empty(modelState);
    }

    [Theory]
    [InlineData(nameof(ValidateSomeProperties.NeverValidBecauseType) + "." + nameof(NeverValid.NeverValidProperty))]
    [InlineData(nameof(ValidateSomeProperties.NeverValidBecauseAttribute))]
    [InlineData(nameof(ValidateSomeProperties.ValidateNever))]
    public async Task PropertyWithinValidateNeverType_IsSkipped(string propertyName)
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateNoProperties),
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request => request.QueryString = new QueryString($"?{propertyName}=1"));

        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        Assert.IsType<ValidateNoProperties>(result.Model);

        Assert.True(modelState.IsValid);
        var kvp = Assert.Single(modelState);
        Assert.Equal(propertyName, kvp.Key);
        var state = kvp.Value;
        Assert.NotNull(state);
        Assert.Equal(ModelValidationState.Skipped, state.ValidationState);
    }

    private class ValidateSometimesAttribute : Attribute, IPropertyValidationFilter
    {
        private readonly string _otherProperty;

        public ValidateSometimesAttribute(string otherProperty)
        {
            // Would null-check otherProperty in real life.
            _otherProperty = otherProperty;
        }

        public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
        {
            if (entry.Metadata.MetadataKind == ModelMetadataKind.Property &&
                parentEntry.Metadata != null)
            {
                // In real life, would throw an InvalidOperationException if otherProperty were null i.e. the
                // property was not known. Could also assert container is non-null (see ValidationVisitor).
                var container = parentEntry.Model;
                var otherProperty = parentEntry.Metadata.Properties[_otherProperty];
                if (otherProperty.PropertyGetter(container) == null)
                {
                    return false;
                }
            }

            return true;
        }
    }

    private class ValidateSomePropertiesSometimes
    {
        public string Control { get; set; }

        [ValidateSometimes(nameof(Control))]
        [Range(0, 10)]
        public int ControlLength => Control.Length;
    }

    [Fact]
    public async Task PropertyToSometimesSkip_IsSkipped_IfControlIsNull()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomePropertiesSometimes),
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
        modelState.SetModelValue(
            nameof(ValidateSomePropertiesSometimes.ControlLength),
            rawValue: null,
            attemptedValue: null);

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomePropertiesSometimes>(result.Model);
        Assert.Null(model.Control);

        // Note this Exception is not thrown earlier.
        Assert.Throws<NullReferenceException>(() => model.ControlLength);

        Assert.True(modelState.IsValid);
        var kvp = Assert.Single(modelState);
        Assert.Equal(nameof(ValidateSomePropertiesSometimes.ControlLength), kvp.Key);
        Assert.Equal(ModelValidationState.Skipped, kvp.Value.ValidationState);
    }

    [Fact]
    public async Task PropertyToSometimesSkip_IsValidated_IfControlIsNotNull()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomePropertiesSometimes),
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request => request.QueryString = new QueryString(
                $"?{nameof(ValidateSomePropertiesSometimes.Control)}=1"));

        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
        modelState.SetModelValue(
            nameof(ValidateSomePropertiesSometimes.ControlLength),
            rawValue: null,
            attemptedValue: null);

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomePropertiesSometimes>(result.Model);
        Assert.Equal("1", model.Control);
        Assert.Equal(1, model.ControlLength);

        Assert.True(modelState.IsValid);
        Assert.Collection(
            modelState,
            state => Assert.Equal(nameof(ValidateSomePropertiesSometimes.Control), state.Key),
            state =>
            {
                Assert.Equal(nameof(ValidateSomePropertiesSometimes.ControlLength), state.Key);
                Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
            });
    }

    // This type has a IPropertyValidationFilter declared on a property, but no validators.
    // We should expect validation to short-circuit
    private class ValidateSomePropertiesSometimesWithoutValidation
    {
        public string Control { get; set; }

        [ValidateSometimes(nameof(Control))]
        public int ControlLength => Control.Length;
    }

    [Fact]
    public async Task PropertyToSometimesSkip_IsNotValidated_IfNoValidationAttributesExistButPropertyValidationFilterExists()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(ValidateSomePropertiesSometimesWithoutValidation),
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var modelState = testContext.ModelState;

        // Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
        modelState.SetModelValue(
            nameof(ValidateSomePropertiesSometimes.ControlLength),
            rawValue: null,
            attemptedValue: null);

        // Act
        var result = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(result.IsModelSet);
        var model = Assert.IsType<ValidateSomePropertiesSometimesWithoutValidation>(result.Model);
        Assert.Null(model.Control);

        // Note this Exception is not thrown earlier.
        Assert.Throws<NullReferenceException>(() => model.ControlLength);

        Assert.True(modelState.IsValid);
        var kvp = Assert.Single(modelState);
        Assert.Equal(nameof(ValidateSomePropertiesSometimesWithoutValidation.ControlLength), kvp.Key);
        Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
    }

    private class Order11
    {
        public IEnumerable<Address> ShippingAddresses { get; set; }

        public Address HomeAddress { get; set; }

        [FromBody]
        public Address OfficeAddress { get; set; }
    }

    private class Address
    {
        public int Street { get; set; }

        public string State { get; set; }

        [Range(10000, 99999)]
        public int Zip { get; set; }

        public Country Country { get; set; }
    }

    private class Country
    {
        public string Name { get; set; }
    }

    [Fact]
    public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels()
    {
        // Arrange
        var parameter = new ParameterDescriptor
        {
            Name = "parameter",
            ParameterType = typeof(Order11)
        };

        var input = "{\"Zip\":\"47\"}";
        var testContext = ModelBindingTestHelper.GetTestContext(
            request =>
            {
                request.QueryString =
                    new QueryString("?HomeAddress.Country.Name=US&ShippingAddresses[0].Zip=45&HomeAddress.Zip=46");
                request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
                request.ContentType = "application/json";
            },
            options =>
            {
                options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Address)));
            });

        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        Assert.Equal(3, modelState.Count);
        Assert.Equal(0, modelState.ErrorCount);
        Assert.True(modelState.IsValid);

        var entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Country.Name").Value;
        Assert.Equal("US", entry.AttemptedValue);
        Assert.Equal("US", entry.RawValue);
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "ShippingAddresses[0].Zip").Value;
        Assert.Equal("45", entry.AttemptedValue);
        Assert.Equal("45", entry.RawValue);
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Zip").Value;
        Assert.Equal("46", entry.AttemptedValue);
        Assert.Equal("46", entry.RawValue);
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
    }

    [Fact]
    public async Task FromBody_JToken_ExcludedFromValidation()
    {
        // Arrange
        var options = new TestMvcOptions().Value;
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(options);
        var parameter = new ParameterDescriptor
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo
            {
                BinderModelName = "CustomParameter",
                BindingSource = BindingSource.Body
            },
            ParameterType = typeof(JToken)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            updateRequest: request =>
            {
                request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello\" }"));
                request.ContentType = "application/json";
            },
            mvcOptions: options);

        var httpContext = testContext.HttpContext;
        var modelState = testContext.ModelState;

        // We need to add another model state entry which should get marked as skipped so
        // we can prove that the JObject was skipped.
        modelState.SetModelValue("CustomParameter.message", "Hello", "Hello");

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);
        Assert.NotNull(modelBindingResult.Model);
        var message = Assert.IsType<JObject>(modelBindingResult.Model).GetValue("message").Value<string>();
        Assert.Equal("Hello", message);

        Assert.True(modelState.IsValid);
        Assert.Single(modelState);

        var entry = Assert.Single(modelState, kvp => kvp.Key == "CustomParameter.message");
        Assert.Equal(ModelValidationState.Skipped, entry.Value.ValidationState);
    }

    // Regression test for https://github.com/aspnet/Mvc/issues/3743
    //
    // A cancellation token that's bound with the empty prefix will end up suppressing
    // the empty prefix. Since the empty prefix is a prefix of everything, this will
    // basically result in clearing out all model errors, which is BAD.
    //
    // The fix is to treat non-user-input as have a key of null, which means that the MSD
    // isn't even examined when it comes to suppressing validation.
    [Fact]
    public async Task CancellationToken_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(new TestMvcOptions().Value);
        var parameter = new ParameterDescriptor
        {
            Name = "cancellationToken",
            ParameterType = typeof(CancellationToken)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();

        var httpContext = testContext.HttpContext;
        var modelState = testContext.ModelState;

        // We need to add another model state entry - we want this to be ignored.
        modelState.SetModelValue("message", "Hello", "Hello");

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);
        Assert.NotNull(modelBindingResult.Model);
        Assert.IsType<CancellationToken>(modelBindingResult.Model);

        Assert.False(modelState.IsValid);
        Assert.Single(modelState);

        var entry = Assert.Single(modelState, kvp => kvp.Key == "message");
        Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
    }

    // Similar to CancellationToken_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors - binding the body
    // with the empty prefix should not cause unrelated modelstate entries to get suppressed.
    [Fact]
    public async Task FromBody_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors_Valid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(new TestMvcOptions().Value);
        var parameter = new ParameterDescriptor
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo
            {
                BindingSource = BindingSource.Body
            },
            ParameterType = typeof(Greeting)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request =>
            {
                request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello\" }"));
                request.ContentType = "application/json";
            });

        var httpContext = testContext.HttpContext;
        var modelState = testContext.ModelState;

        // We need to add another model state entry which should not get changed.
        modelState.SetModelValue("other.key", "1", "1");

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);
        Assert.NotNull(modelBindingResult.Model);
        var message = Assert.IsType<Greeting>(modelBindingResult.Model).Message;
        Assert.Equal("Hello", message);

        Assert.False(modelState.IsValid);
        Assert.Single(modelState);

        var entry = Assert.Single(modelState, kvp => kvp.Key == "other.key");
        Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
    }

    // Similar to CancellationToken_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors - binding the body
    // with the empty prefix should not cause unrelated modelstate entries to get suppressed.
    [Fact]
    public async Task FromBody_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors_Invalid()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(new TestMvcOptions().Value);
        var parameter = new ParameterDescriptor
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo
            {
                BindingSource = BindingSource.Body
            },
            ParameterType = typeof(Greeting)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(
            request =>
            {
                // This string is too long and will have a validation error.
                request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello There\" }"));
                request.ContentType = "application/json";
            });

        var httpContext = testContext.HttpContext;
        var modelState = testContext.ModelState;

        // We need to add another model state entry which should not get changed.
        modelState.SetModelValue("other.key", "1", "1");

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);
        Assert.NotNull(modelBindingResult.Model);
        var message = Assert.IsType<Greeting>(modelBindingResult.Model).Message;
        Assert.Equal("Hello There", message);

        Assert.False(modelState.IsValid);
        Assert.Equal(2, modelState.Count);

        var entry = Assert.Single(modelState, kvp => kvp.Key == "Message");
        Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);

        entry = Assert.Single(modelState, kvp => kvp.Key == "other.key");
        Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
    }

    private class Greeting
    {
        [StringLength(5)]
        public string Message { get; set; }
    }

    [Fact]
    public async Task Validation_NoAttributeInGraphOfObjects_WithDefaultValidatorProviders()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Order12),
            BindingInfo = new BindingInfo
            {
                BindingSource = BindingSource.Body
            },
        };

        var input = new Order12
        {
            Id = 10,
            OrderFile = new byte[40],
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(input)));
            request.ContentType = "application/json";
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Order12>(modelBindingResult.Model);
        Assert.Equal(input.Id, model.Id);
        Assert.Equal(input.OrderFile, model.OrderFile);
        Assert.Null(model.RelatedOrders);

        Assert.Empty(modelState);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
    }

    private class Order12
    {
        public int Id { get; set; }

        public byte[] OrderFile { get; set; }

        public IList<Order12> RelatedOrders { get; set; }
    }

    [Fact]
    public async Task Validation_ListOfType_NoValidatorOnParameter()
    {
        // Arrange
        var parameterInfo = GetType().GetMethod(nameof(Validation_ListOfType_NoValidatorOnParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
            .GetParameters()
            .First();

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);

        var parameter = new ParameterDescriptor()
        {
            Name = parameterInfo.Name,
            ParameterType = parameterInfo.ParameterType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?[0]=1&[1]=2");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<int>>(modelBindingResult.Model);
        Assert.Equal(new[] { 1, 2 }, model);

        Assert.False(modelMetadata.HasValidators);

        Assert.True(modelState.IsValid);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "[0]").Value;
        Assert.Equal("1", entry.AttemptedValue);
        Assert.Equal("1", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "[1]").Value;
        Assert.Equal("2", entry.AttemptedValue);
        Assert.Equal("2", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    private static void Validation_ListOfType_NoValidatorOnParameterTestMethod(List<int> parameter) { }

    [Fact]
    public async Task Validation_ListOfType_ValidatorOnParameter()
    {
        // Arrange
        var parameterInfo = GetType().GetMethod(nameof(Validation_ListOfType_ValidatorOnParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
            .GetParameters()
            .First();

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);

        var parameter = new ParameterDescriptor()
        {
            Name = parameterInfo.Name,
            ParameterType = parameterInfo.ParameterType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?[0]=1&[1]=2");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<List<int>>(modelBindingResult.Model);
        Assert.Equal(new[] { 1, 2 }, model);

        Assert.True(modelMetadata.HasValidators);

        Assert.False(modelState.IsValid);
        Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "").Value;
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "[0]").Value;
        Assert.Equal("1", entry.AttemptedValue);
        Assert.Equal("1", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "[1]").Value;
        Assert.Equal("2", entry.AttemptedValue);
        Assert.Equal("2", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    private static void Validation_ListOfType_ValidatorOnParameterTestMethod([ConsistentMinLength(3)] List<int> parameter) { }

    private class ConsistentMinLength : ValidationAttribute
    {
        private readonly int _length;

        public ConsistentMinLength(int length)
        {
            _length = length;
        }

        public override bool IsValid(object value)
        {
            return value is ICollection collection && collection.Count >= _length;
        }
    }

    [Fact]
    public async Task Validation_CollectionOfType_ValidatorOnElement()
    {
        // Arrange
        var parameterInfo = GetType().GetMethod(nameof(Validation_CollectionOfType_ValidatorOnElementTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
            .GetParameters()
            .First();

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);

        var parameter = new ParameterDescriptor()
        {
            Name = parameterInfo.Name,
            ParameterType = parameterInfo.ParameterType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?p[0].Id=1&p[1].Id=2");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Collection<InvalidEvenIds>>(modelBindingResult.Model);
        Assert.Equal(1, model[0].Id);
        Assert.Equal(2, model[1].Id);

        Assert.True(modelMetadata.HasValidators);

        Assert.False(modelState.IsValid);
        Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "p[0].Id").Value;
        Assert.Equal("1", entry.AttemptedValue);
        Assert.Equal("1", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "p[1]").Value;
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "p[1].Id").Value;
        Assert.Equal("2", entry.AttemptedValue);
        Assert.Equal("2", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    private static void Validation_CollectionOfType_ValidatorOnElementTestMethod(Collection<InvalidEvenIds> p) { }

    public class InvalidEvenIds : IValidatableObject
    {
        public int Id { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Id % 2 == 0)
            {
                yield return new ValidationResult("Failed validation");
            }
        }
    }

    [Fact]
    public async Task Validation_DictionaryType_NoValidators()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(IDictionary<string, int>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value=10");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
        Assert.Collection(
            model.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal("key0", kvp.Key);
                Assert.Equal(10, kvp.Value);
            });

        Assert.True(modelState.IsValid);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value;
        Assert.Equal("key0", entry.AttemptedValue);
        Assert.Equal("key0", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value;
        Assert.Equal("10", entry.AttemptedValue);
        Assert.Equal("10", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    [Fact]
    public async Task Validation_DictionaryType_ValueHasValidators()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(Dictionary<string, NeverValid>)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value.NeverValidProperty=value0");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Dictionary<string, NeverValid>>(modelBindingResult.Model);
        Assert.Collection(
            model.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal("key0", kvp.Key);
                Assert.Equal("value0", kvp.Value.NeverValidProperty);
            });

        Assert.False(modelState.IsValid);
        Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value;
        Assert.Equal("key0", entry.AttemptedValue);
        Assert.Equal("key0", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value.NeverValidProperty").Value;
        Assert.Equal("value0", entry.AttemptedValue);
        Assert.Equal("value0", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value;
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        Assert.Single(entry.Errors);
    }

    [Fact]
    public async Task Validation_TopLevelProperty_NoValidation()
    {
        // Arrange
        var modelType = typeof(Validation_TopLevelPropertyController);
        var propertyInfo = modelType.GetProperty(nameof(Validation_TopLevelPropertyController.Model));

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, propertyInfo.PropertyType);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);

        var parameter = new ParameterDescriptor()
        {
            Name = propertyInfo.Name,
            ParameterType = propertyInfo.PropertyType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Model.Id=12");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Validation_TopLevelPropertyModel>(modelBindingResult.Model);
        Assert.Equal(12, model.Id);

        Assert.False(modelMetadata.HasValidators);

        Assert.True(modelState.IsValid);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "Model.Id").Value;
        Assert.Equal("12", entry.AttemptedValue);
        Assert.Equal("12", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    public class Validation_TopLevelPropertyModel
    {
        public int Id { get; set; }
    }

    private class Validation_TopLevelPropertyController
    {
        public Validation_TopLevelPropertyModel Model { get; set; }
    }

    [Fact]
    public async Task Validation_TopLevelProperty_ValidationOnProperty()
    {
        // Arrange
        var modelType = typeof(Validation_TopLevelProperty_ValidationOnPropertyController);
        var propertyInfo = modelType.GetProperty(nameof(Validation_TopLevelProperty_ValidationOnPropertyController.Model));

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, propertyInfo.PropertyType);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);

        var parameter = new ParameterDescriptor()
        {
            Name = propertyInfo.Name,
            ParameterType = propertyInfo.PropertyType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Model.Id=12");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<Validation_TopLevelPropertyModel>(modelBindingResult.Model);
        Assert.Equal(12, model.Id);

        Assert.True(modelMetadata.HasValidators);

        Assert.False(modelState.IsValid);
        Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "Model.Id").Value;
        Assert.Equal("12", entry.AttemptedValue);
        Assert.Equal("12", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = Assert.Single(modelState, e => e.Key == "Model").Value;
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
    }

    public class Validation_TopLevelProperty_ValidationOnPropertyController
    {
        [CustomValidation(typeof(Validation_TopLevelProperty_ValidationOnPropertyController), nameof(Validate))]
        public Validation_TopLevelPropertyModel Model { get; set; }

        public static ValidationResult Validate(ValidationContext context)
        {
            return new ValidationResult("Invalid result");
        }
    }

    [Fact]
    public async Task Validation_InfinitelyRecursiveType_NoValidators()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "parameter",
            ParameterType = typeof(RecursiveModel)
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Property1=8");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<RecursiveModel>(modelBindingResult.Model);
        Assert.Equal(8, model.Property1);

        Assert.True(modelState.IsValid);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "Property1").Value;
        Assert.Equal("8", entry.AttemptedValue);
        Assert.Equal("8", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    public class RecursiveModel
    {
        public int Property1 { get; set; }

        public RecursiveModel Property2 { get; set; }

        public RecursiveModel Property3 => new RecursiveModel { Property1 = Property1 };
    }

    [Fact]
    public async Task Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameter()
    {
        // Arrange
        var parameterInfo = GetType().GetMethod(nameof(Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod), BindingFlags.NonPublic | BindingFlags.Static)
            .GetParameters()
            .First();

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);

        var parameter = new ParameterDescriptor()
        {
            Name = parameterInfo.Name,
            ParameterType = parameterInfo.ParameterType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Property1=8");
        });

        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<RecursiveModel>(modelBindingResult.Model);
        Assert.Equal(8, model.Property1);

        Assert.True(modelState.IsValid);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);

        var entry = Assert.Single(modelState, e => e.Key == "Property1").Value;
        Assert.Equal("8", entry.AttemptedValue);
        Assert.Equal("8", entry.RawValue);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    private static void Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod([Required] RecursiveModel model) { }

    [Fact]
    public async Task Validation_ModelWithNonNullableReferenceTypes_DoesNotValidateNonNullablePropertiesOnFrameworkTypes()
    {
        // Arrange
        var parameterInfo = GetType().GetMethod(nameof(Validation_ModelWithNonNullableReferenceTypes_DoesNotValidateNonNullablePropertiesOnFrameworkTypesAction), BindingFlags.NonPublic | BindingFlags.Static)
            .GetParameters()
            .First();

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var services = ModelBindingTestHelper.GetServices(modelMetadataProvider);
        var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
        var options = services.GetRequiredService<IOptions<MvcOptions>>().Value;
        var validator = new RecordingObjectValidator(
            modelMetadataProvider,
             TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders,
             options);
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider, mvcOptions: options, validator: validator);

        var parameter = new ParameterDescriptor()
        {
            Name = parameterInfo.Name,
            ParameterType = parameterInfo.ParameterType,
        };

        var testContext = ModelBindingTestHelper.GetTestContext(request =>
        {
            request.QueryString = new QueryString("?Name=CoolName");
        });
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);

        // Assert
        Assert.True(modelBindingResult.IsModelSet);

        var model = Assert.IsType<ModelWithNonNullableReferenceTypeProperties>(modelBindingResult.Model);
        Assert.Equal("CoolName", model.Name);

        Assert.True(modelState.IsValid);
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);

        var visited = validator.ValidationVisitor.Visited;
        Assert.Collection(
            visited,
            v => Assert.Equal(typeof(ModelWithNonNullableReferenceTypeProperties), v.ModelType),
            v => Assert.Equal(typeof(string), v.ModelType),
            v => Assert.Equal(typeof(Delegate), v.ModelType));
    }

#nullable enable
    private static void Validation_ModelWithNonNullableReferenceTypes_DoesNotValidateNonNullablePropertiesOnFrameworkTypesAction(ModelWithNonNullableReferenceTypeProperties model) { }

    public class ModelWithNonNullableReferenceTypeProperties
    {
        public string Name { get; set; } = default!;

        public Delegate Delegate { get; set; } = typeof(ModelWithNonNullableReferenceTypeProperties).GetMethod(nameof(SomeMethod))!.CreateDelegate<Action>();

        public static void SomeMethod() { }
    }
#nullable restore

    private static void AssertRequiredError(string key, ModelError error)
    {
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage(key), error.ErrorMessage);
        Assert.Null(error.Exception);
    }

    private class RecordingObjectValidator : DefaultObjectValidator
    {
        public RecordingObjectValidator(IModelMetadataProvider modelMetadataProvider, IList<IModelValidatorProvider> validatorProviders, MvcOptions mvcOptions)
            : base(modelMetadataProvider, validatorProviders, mvcOptions)
        {
        }

        public RecordingValidationVisitor ValidationVisitor { get; private set; }

        public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState)
        {
            ValidationVisitor = new RecordingValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState);
            return ValidationVisitor;
        }
    }

    private class RecordingValidationVisitor : ValidationVisitor
    {
        public RecordingValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState)
            : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState)
        {
        }

        public List<ModelMetadata> Visited = new();

        protected override bool Visit(ModelMetadata metadata, string key, object model)
        {
            Visited.Add(metadata);
            return base.Visit(metadata, key, model);
        }
    }
}
